package com.moon.java.code.analyzer;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

import com.moon.java.code.analyzer.JavaFileTraversal.MethodFieldInfo;
import com.moon.java.code.analyzer.MyBatisXMLParser.MapperInfo;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.bytecode.BadBytecode;
import javassist.bytecode.BootstrapMethodsAttribute;
import javassist.bytecode.BootstrapMethodsAttribute.BootstrapMethod;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.CodeIterator;
import javassist.bytecode.ConstPool;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.Opcode;

/**
 * 方法调用栈显示，结果：<类名,List<方法名>>
 * 
 * @Description:
 * @author wangzhiyue
 * @date 2025/02/17 16:22:16
 */
public class MethodCallAnalyzer {
    // 用于记录方法调用关系的图<方法，方法中调用的其他方法>
    private static Map<String, List<MethodDescInfo>> callGraph = new TreeMap<>();
    // 缓存接口及其实现类
    private static Map<String, List<CtClass>> interfaceImplCache = new HashMap<>();
    // 用于记录方法中使用的字段信息<方法，方法中使用的字段>
    private static Map<String, Set<FieldInfo>> fieldUsageGraph = new TreeMap<>();
    // 只筛选包含接口的特定实现类
    public static Map<String, Set<String>> containImplClass = new HashMap<>();

    public static void analyzeMethodCalls(MethodDescInfo method, List<Predicate<MethodDescInfo>> filters) throws NotFoundException {
        String fullMethod = method.fullName();
        if (callGraph.containsKey(fullMethod)) {
            return;
        }
        List<MethodDescInfo> calledMethods = new ArrayList<MethodCallAnalyzer.MethodDescInfo>();
        calledMethods.add(method);
        callGraph.put(fullMethod, calledMethods);
        if (method.isInterface) {
            return;
        }
        List<MethodDescInfo> otherMethos = findCalledMethods(method, filters);
        calledMethods.addAll(otherMethos);
        // 递归分析被调用的方法
        for (MethodDescInfo calledMethod : otherMethos) {
            analyzeMethodCalls(calledMethod, filters);
        }
    }

    public static List<MethodDescInfo> findCalledMethods(MethodDescInfo method, List<Predicate<MethodDescInfo>> filters) throws NotFoundException {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get(method.className);
        // 查找所有以 lambda$ 开头的方法
        List<CtMethod> lambdaMethods = new ArrayList<>();
        CtMethod[] allMethods = cc.getDeclaredMethods();
        for (CtMethod m : allMethods) {
            if (m.getName().startsWith("lambda$")) {
                lambdaMethods.add(m);
            }
        }
        // 从类文件获取BootstrapMethods属性
        BootstrapMethodsAttribute bsmAttr = (BootstrapMethodsAttribute) cc.getClassFile().getAttribute(BootstrapMethodsAttribute.tag);
        CtField[] fields = cc.getDeclaredFields();
        CtMethod cm = null;
        if (StringUtils.isBlank(method.methodDesc)) {
            cm = cc.getDeclaredMethod(method.methodNameCalled);
        } else {
            cm = cc.getMethod(method.methodNameCalled, method.methodDesc);
        }
        List<MethodDescInfo> list = analyzerMethodInfo(cm, pool, filters, bsmAttr, lambdaMethods, fields, method);
        return list;
    }

    private static List<MethodDescInfo> analyzerMethodInfo(CtMethod cm, ClassPool pool, List<Predicate<MethodDescInfo>> filters, BootstrapMethodsAttribute bsmAttr, List<CtMethod> lambdaMethods,
            CtField[] fields, MethodDescInfo method) {
        MethodInfo methodInfo = cm.getMethodInfo();
        CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
        CodeIterator iterator = codeAttribute.iterator();
        List<MethodDescInfo> calledMethods = new ArrayList<>();
        ConstPool constPool = methodInfo.getConstPool();

        while (iterator.hasNext()) {
            try {
                int index = iterator.next();
                int opcode = iterator.byteAt(index);

                if (opcode == Opcode.INVOKEINTERFACE || opcode == Opcode.INVOKEVIRTUAL || opcode == Opcode.INVOKESTATIC || opcode == Opcode.INVOKESPECIAL) {
                    if (index + 1 >= iterator.getCodeLength()) {
                        System.err.println("Index out of bounds at index " + index + " for opcode " + opcode);
                        continue;
                    }
                    int indexOfMethodRef = iterator.u16bitAt(index + 1);
                    List<MethodDescInfo> methods = getMethodInfo(constPool, indexOfMethodRef, pool, opcode, filters);
                    calledMethods.addAll(methods);
                } else if (opcode == Opcode.INVOKEDYNAMIC) {
                    CtMethod ctMethod = findMethodInLambda(iterator, constPool, index, lambdaMethods, bsmAttr);
                    if (ctMethod != null) {
                        calledMethods.addAll(analyzerMethodInfo(ctMethod, pool, filters, bsmAttr, lambdaMethods, fields, method));
                    }
                }
            } catch (BadBytecode e) {
                e.printStackTrace();
            } catch (ArrayIndexOutOfBoundsException e) {
                System.err.println("Array index out of bounds: " + e.getMessage());
            }
        }
        // 分析方法所在类的字段使用情况
        for (CtField field : fields) {
            if (isFieldUsedInMethod(cm, field)) {
                try {
                    fieldUsageGraph.computeIfAbsent(method.className, v -> new HashSet<>()).add(new FieldInfo(field.getName(), field.getType().getName()));
                } catch (NotFoundException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        return calledMethods;
    }

    private static class FieldInfo {
        private String fieldName;
        private String className;

        public FieldInfo(String fieldName, String className) {
            super();
            this.fieldName = fieldName;
            this.className = className;
        }

        public String getFieldName() {
            return fieldName;
        }

        public void setFieldName(String fieldName) {
            this.fieldName = fieldName;
        }

        public String getClassName() {
            return className;
        }

        public void setClassName(String className) {
            this.className = className;
        }

        @Override
        public int hashCode() {
            return Objects.hash(className, fieldName);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            FieldInfo other = (FieldInfo) obj;
            return Objects.equals(className, other.className) && Objects.equals(fieldName, other.fieldName);
        }

    }

    private static boolean isFieldUsedInMethod(CtMethod method, CtField field) {
        try {
            MethodInfo methodInfo = method.getMethodInfo();
            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
            CodeIterator iterator = codeAttribute.iterator();
            ConstPool constPool = methodInfo.getConstPool();

            while (iterator.hasNext()) {
                int index = iterator.next();
                int opcode = iterator.byteAt(index);

                if (opcode == Opcode.GETFIELD || opcode == Opcode.PUTFIELD || opcode == Opcode.GETSTATIC || opcode == Opcode.PUTSTATIC) {
                    int indexOfFieldRef = iterator.u16bitAt(index + 1);
                    int classIndex = constPool.getFieldrefClass(indexOfFieldRef);
                    int nameAndTypeIndex = constPool.getFieldrefNameAndType(indexOfFieldRef);
                    String fieldClassName = constPool.getClassInfo(classIndex);
                    int fieldNameIndex = constPool.getNameAndTypeName(nameAndTypeIndex);
                    String fieldName = constPool.getUtf8Info(fieldNameIndex);

                    if (fieldClassName.equals(field.getDeclaringClass().getName()) && fieldName.equals(field.getName())) {
                        return true;
                    }
                }
            }
        } catch (BadBytecode e) {
            e.printStackTrace();
        }

        return false;
    }

    private static List<CtClass> findImplementingClasses(ClassPool pool, String interfaceName) {
        if (interfaceImplCache.containsKey(interfaceName)) {
            return interfaceImplCache.get(interfaceName);
        }
        List<CtClass> implClasses = new ArrayList<>();
        try {
            CtClass interfaceClass = pool.get(interfaceName);
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            URL[] urls = ((URLClassLoader) classLoader).getURLs();
            for (URL url : urls) {
                if (url.getProtocol().equals("file")) {
                    File file = new File(url.getFile());
                    if (file.isDirectory()) {
                        scanDirectory(pool, file, "", implClasses, interfaceClass);
                    }
                }
            }
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        interfaceImplCache.put(interfaceName, implClasses);
        return implClasses;
    }

    private static void scanDirectory(ClassPool pool, File directory, String packageName, List<CtClass> implClasses, CtClass interfaceClass) throws NotFoundException {
        File[] files = directory.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    scanDirectory(pool, file, packageName + file.getName() + ".", implClasses, interfaceClass);
                } else if (file.getName().endsWith(".class")) {
                    String className = packageName + file.getName().substring(0, file.getName().length() - 6);
                    CtClass clazz = pool.get(className);
                    if (clazz.subtypeOf(interfaceClass) && !clazz.isInterface()) {
                        implClasses.add(clazz);
                    }
                }
            }
        }
    }

    public static void printGroupedResults(List<Predicate<MethodDescInfo>> filterOutMethods, Map<String, MethodFieldInfo> existJavaInfoMap, Map<String, MapperInfo> existMapperInfoMap,
            List<String> checkXmlPackageMethodName, List<String> checkXmlPackageFieldName, Set<String> existDubboClient) {
        Map<String, List<MethodDescInfo>> classMethods = callGraph.values().stream().flatMap(v -> v.stream()).collect(Collectors.groupingBy(v -> v.className, TreeMap::new, Collectors.toList()));

        for (Map.Entry<String, List<MethodDescInfo>> classMethodEntry : classMethods.entrySet()) {
            String className = classMethodEntry.getKey();
            List<MethodDescInfo> calledMethods = classMethodEntry.getValue();
            Set<String> useMethod = new TreeSet<String>();
            for (MethodDescInfo methodDescInfo : calledMethods) {
                if (methodDescInfo.className.equals(className) && (CollectionUtils.isEmpty(filterOutMethods) || !isValid(methodDescInfo, filterOutMethods))) {
                    useMethod.add(methodDescInfo.methodNameCalled);
                }
            }
            int endIndex = className.length();
            if (className.endsWith("Impl")) {
                endIndex = endIndex - 4;
            }
            String singleClassName = className.substring(className.lastIndexOf(".") + 1, endIndex);
            List<String> error = new ArrayList<String>(2);
            MethodFieldInfo existJavaInfo = existJavaInfoMap.get(singleClassName);
            MapperInfo existMapperInfo = existMapperInfoMap.get(singleClassName);
            String javaError = "[JAVA]";
            String xmlError = "[XML]";
            String dubboError = "[dubbo]";
            if (existJavaInfo == null) {
                error.add(javaError);
            }
            List<String> checkXmlPackage = new ArrayList<String>();
            checkXmlPackage.addAll(checkXmlPackageFieldName);
            checkXmlPackage.addAll(checkXmlPackageMethodName);
            if (isCheckXml(className, checkXmlPackage) && existMapperInfo == null) {
                error.add(xmlError);
            }
            if (!error.isEmpty()) {
                System.out.println(className + "  " + StringUtils.join(error, ""));
            } else {
                System.out.println(className);
            }
            if (!useMethod.isEmpty()) {
                System.out.println("  useMethods：");
                for (String useMethodName : useMethod) {
                    error.clear();
                    String info = "    " + useMethodName;
                    if (existJavaInfo != null && !isExistJavaMethod(existJavaInfo, useMethodName)) {
                        error.add(javaError);
                    }
                    if (isCheckXml(className, checkXmlPackageMethodName) && existMapperInfo != null && !isExistXmlMethod(existMapperInfo, useMethodName)) {
                        error.add(xmlError);
                    }
                    if (!error.isEmpty()) {
                        System.out.println(info + "  " + StringUtils.join(error, ""));
                    } else {
                        System.out.println(info);
                    }
                }
            }

            // 打印方法中使用的字段信息
            Set<FieldInfo> usedFields = fieldUsageGraph.getOrDefault(className, new HashSet<>());
            if (!usedFields.isEmpty()) {
                System.out.println("  usedFields：");
                for (FieldInfo field : usedFields) {
                    error.clear();
                    String info = "    " + field.getFieldName();
                    if (existJavaInfo != null && !isExistJavaField(existJavaInfo, field.getFieldName())) {
                        error.add(javaError);
                    }
                    if (isCheckXml(className, checkXmlPackageFieldName) && existMapperInfo != null && !isExistXmlField(singleClassName, existMapperInfo, field.getFieldName())) {
                        error.add(xmlError);
                    }
                    if (field.getClassName().contains("dubbo.") && !existDubboClient.contains(field.getClassName())) {
                        error.add(dubboError);
                    }
                    if (!error.isEmpty()) {
                        System.out.println(info + "  " + StringUtils.join(error, ""));
                    } else {
                        System.out.println(info);
                    }
                }
            }
        }
    }

    private static boolean isExistJavaField(MethodFieldInfo existJavaInfo, String field) {
        return existJavaInfo != null && existJavaInfo.getFieldNames().contains(field);
    }

    private static boolean isExistJavaMethod(MethodFieldInfo existJavaInfo, String useMethodName) {
        return existJavaInfo != null && existJavaInfo.getMethodNames().contains(useMethodName);
    }

    private static boolean isExistXmlField(String singleClassName, MapperInfo info, String field) {
        return info != null && info.getResultMapInfos().stream().filter(v -> v.getType().equals(singleClassName)).anyMatch(v -> v.getProperties().contains(field));
    }

    private static boolean isExistXmlMethod(MapperInfo info, String useMethodName) {
        return info != null && info.getMethods().contains(useMethodName);
    }

    private static boolean isCheckXml(String className, List<String> packageName) {
        return packageName.stream().anyMatch(v -> className.startsWith(v));
    }

    private static List<MethodDescInfo> getMethodInfo(ConstPool constPool, int indexOfMethodRef, ClassPool pool, int opcode, List<Predicate<MethodDescInfo>> filters) {
        List<MethodDescInfo> methods = new ArrayList<MethodCallAnalyzer.MethodDescInfo>();
        int classIndex = constPool.getMemberClass(indexOfMethodRef);
        int nameAndTypeIndex = constPool.getMemberNameAndType(indexOfMethodRef);
        String declaringClass = constPool.getClassInfo(classIndex);
        int methodNameIndex = constPool.getNameAndTypeName(nameAndTypeIndex);
        int methodDescIndex = constPool.getNameAndTypeDescriptor(nameAndTypeIndex);
        String methodNameCalled = constPool.getUtf8Info(methodNameIndex);
        String methodDesc = constPool.getUtf8Info(methodDescIndex);
        if (opcode == Opcode.INVOKEINTERFACE) {
            // 查找接口的实现类
            List<CtClass> implClasses = findImplementingClasses(pool, declaringClass);
            if (implClasses.isEmpty()) {
                methods.add(new MethodDescInfo(declaringClass, methodNameCalled, true));
            } else {
                for (CtClass implClass : implClasses) {
                    try {
                        implClass.getMethod(methodNameCalled, methodDesc);
                        if (!containImplClass.containsKey(declaringClass) || containImplClass.get(declaringClass).contains(implClass.getName())) {
                            // 如果是接口的实现类且在指定的实现类列表中，则添加到结果中
                            methods.add(new MethodDescInfo(implClass.getName(), methodNameCalled, methodDesc));
                        }
                    } catch (NotFoundException e) {
                        // 忽略未实现该方法的类
                    }
                }
            }
        } else {
            methods.add(new MethodDescInfo(declaringClass, methodNameCalled, methodDesc));
        }
        return methods.stream().filter(v -> isValid(v, filters)).collect(Collectors.toList());
    }

    private static boolean isValid(MethodDescInfo methodInfo, List<Predicate<MethodDescInfo>> filters) {
        return filters.stream().allMatch(filter -> filter.test(methodInfo));
    }

    public static class MethodDescInfo {
        private String className;
        private String methodNameCalled;
        private boolean isInterface;
        private String methodDesc;

        public MethodDescInfo(String className, String methodNameCalled, String methodDesc) {
            super();
            this.className = className;
            this.methodNameCalled = methodNameCalled;
            this.methodDesc = methodDesc;
        }

        public MethodDescInfo(String className, String methodNameCalled, boolean isInterface) {
            super();
            this.className = className;
            this.methodNameCalled = methodNameCalled;
            this.isInterface = isInterface;
        }

        public String fullName() {
            return className + "." + methodNameCalled;
        }

        @Override
        public String toString() {
            return fullName();
        }

    }

    public static class NotClassEqualFilter implements Predicate<MethodDescInfo> {

        private List<String> classNames;

        public NotClassEqualFilter(String... classNames) {
            super();
            this.classNames = Arrays.asList(classNames);
        }

        @Override
        public boolean test(MethodDescInfo t) {
            return !classNames.stream().anyMatch(v -> t.className.equals(v));
        }
    }

    public static class ClassPrefixFilter implements Predicate<MethodDescInfo> {

        private List<String> refixs;

        public ClassPrefixFilter(String... refixs) {
            super();
            this.refixs = Arrays.asList(refixs);
        }

        @Override
        public boolean test(MethodDescInfo t) {
            return refixs.stream().anyMatch(v -> t.className.startsWith(v));
        }
    }

    public static class NotClassPrefixFilter implements Predicate<MethodDescInfo> {

        private List<String> refixs;

        public NotClassPrefixFilter(String... refixs) {
            super();
            this.refixs = Arrays.asList(refixs);
        }

        @Override
        public boolean test(MethodDescInfo t) {
            return !refixs.stream().anyMatch(v -> t.className.startsWith(v));
        }
    }

    public static class NotMethodPrefixFilter implements Predicate<MethodDescInfo> {

        private List<String> refixs;

        public NotMethodPrefixFilter(String... refixs) {
            super();
            this.refixs = Arrays.asList(refixs);
        }

        @Override
        public boolean test(MethodDescInfo t) {
            return !refixs.stream().anyMatch(v -> t.methodNameCalled.startsWith(v));
        }
    }

    public static MapperInfo replaceTypeName(String desName, String sourceName, Map<String, MapperInfo> existMapperInfoMap) {
        MapperInfo info = existMapperInfoMap.get(sourceName);
        if (info != null) {
            existMapperInfoMap.put(desName, info);
            info.getResultMapInfos().stream().filter(v -> v.getType().equals(sourceName)).forEach(v -> v.setType(desName));
        }
        return info;
    }

    public static void printCallGraph(MethodDescInfo info, int deep, Set<String> printed) {
        if (!printed.add(info.fullName())) {
            return;
        }
        System.out.println("【" + deep + "】" + buildBlank(deep * 2) + info.fullName());
        List<MethodDescInfo> calls = callGraph.get(info.fullName());
        if (calls != null) {
            for (MethodDescInfo sub : calls) {
                printCallGraph(sub, deep + 1, printed);
            }
        }
    }

    private static CtMethod findMethodInLambda(CodeIterator iterator, ConstPool constPool, int index, List<CtMethod> lambdaMethods, BootstrapMethodsAttribute bsmAttr) {

        if (bsmAttr == null) {
            // System.out.println("警告：类文件中没有BootstrapMethods属性");
            return null;
        }
        // 读取 invokedynamic 的操作数（指向常量池中的 InvokeDynamic 条目）
        int invokeDynamicIndex = iterator.u16bitAt(index + 1);

        // 从常量池获取真正的 bootstrap 方法索引
        int bsmIndex = constPool.getInvokeDynamicBootstrap(invokeDynamicIndex);
        // System.out.println("发现invokedynamic指令，常量池索引=" + invokeDynamicIndex + ", bsmIndex=" + bsmIndex);

        // 检查索引是否有效
        if (bsmIndex >= bsmAttr.getMethods().length) {
            System.out.println("错误：bootstrap方法索引越界，最大应为 " + (bsmAttr.getMethods().length - 1));
            return null;
        }

        BootstrapMethod bsmMethod = bsmAttr.getMethods()[bsmIndex];
        CtMethod lambdaMethod = findLambdaMethod(bsmMethod, constPool, lambdaMethods);
        return lambdaMethod;
    }

    private static CtMethod findLambdaMethod(BootstrapMethod bsmMethod, ConstPool constPool, List<CtMethod> lambdaMethods) {
        // 获取Bootstrap方法的参数
        int[] args = bsmMethod.arguments;

        for (int arg : args) {
            if (constPool.getTag(arg) == ConstPool.CONST_MethodHandle) {
                int refKind = constPool.getMethodHandleKind(arg);
                int refIndex = constPool.getMethodHandleIndex(arg);

                // System.out.println("处理方法句柄: kind=" + refKind + ", index=" + refIndex);

                // REF_invokeStatic=6, REF_invokeSpecial=5
                if (refKind == ConstPool.REF_invokeSpecial || refKind == ConstPool.REF_invokeStatic) {
                    String methodName = constPool.getMethodrefName(refIndex);
                    // System.out.println("查找方法: " + className + "." + methodName);

                    // 在lambda方法列表中查找匹配的方法
                    for (CtMethod lambdaMethod : lambdaMethods) {
                        if (lambdaMethod.getName().equals(methodName)) {
                            return lambdaMethod;
                        }
                    }
                }
            }
        }
        return null;
    }

    public static String buildBlank(int count) {
        StringBuilder sb = new StringBuilder("");
        for (int i = 0; i < count; i++) {
            sb.append(" ");
        }
        return sb.toString();
    }
}